package cn.demoncat.util.wx.mp.util;

import cn.demoncat.util.exception.BizRuntimeException;
import cn.demoncat.util.lang.JsonUtil;
import cn.demoncat.util.lang.StringUtil;
import cn.demoncat.util.lang.entity.MapSo;
import cn.demoncat.util.lang.entity.MapSs;
import cn.demoncat.util.redis.util.RedisUtil;
import cn.demoncat.util.web.HttpClientUtil;
import cn.demoncat.util.wx.mp.config.WxMiniProgramProperties;
import cn.demoncat.util.wx.mp.constant.WxMiniProgramConstant;
import cn.demoncat.util.wx.mp.constant.WxMiniProgramLogConstant;
import cn.demoncat.util.wx.mp.constant.WxMiniProgramRedisConstant;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 微信小程序服务端接口工具
 * 
 * @author 延晓磊
 *
 * @since 2019年8月22日
 */
@Component
public class WxMiniProgramUtil {

	@Resource
	private WxMiniProgramProperties wxMiniProgramProperties;
	@Resource
	private RedisUtil redisUtil;
	
	
	private static final Logger LOG = LoggerFactory.getLogger(WxMiniProgramUtil.class);


	/**
	 * 获取access_token
	 *
	 * <pre>
	 * 接口：
	 * auth.getAccessToken
	 * 流程：
	 * 1、服务端请求微信端，获取access_token（部分微信端接口的请求凭证）
	 * 2、默认有效期为7200秒，每次请求都会生成新token替换老token，但替换后5分钟内原token还可以使用（各部署环境要用不同的App，以免token互斥）
	 * 3、本方法是获取、保存（Redis）、刷新token的唯一入口，在请求微信端接口时统一通过调用本方法获取token
	 * </pre>
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年8月22日
	 */
	public String getAccessToken() {
		// 通过缓存获取
		String key = WxMiniProgramRedisConstant.ACCESS_TOKEN.key( wxMiniProgramProperties.getAppid());
		String token = redisUtil.get(key);
		// 不存在重新获取
		if (StringUtils.isBlank(token)) {
			// 发起请求
			JSONObject res = doGet(WxMiniProgramConstant.Urls.ACCESS_TOKEN);
			// 缓存AccessToken
			token = res.getString("access_token");
			redisUtil.setEx(key, token, res.getInteger("expires_in") - WxMiniProgramRedisConstant.ACCESS_TOKEN.getExp());
		}
		return token;
	}

	/**
	 * 获取用户的微信登录信息
	 * 
	 * <pre>
	 * 接口：
	 * auth.code2Session
	 * 流程：
	 * 1、小程序通过wx.login登录，获取登录凭证 res.code
	 * 2、服务端请求微信端，获取微信用户的openid
 	 * 3、服务端根据openid绑定或查询用户，完成用户登录/创建
	 * </pre>
	 * 
	 * @param code	小程序【通过wx.login】获取的“微信登录凭证”
	 *
	 * @return openid：微信用户在小程序的唯一标识
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年8月22日
	 */
	public String login(String code) {
		return doGet(WxMiniProgramConstant.Urls.LOGIN + code).getString("openid");
	}

	/**
	 * 获取用户手机号
	 *
	 * <pre>
	 * 接口：
	 * phonenumber.getPhoneNumber
	 * 资质：
	 * 1、非个人开发者，完成了认证的小程序
	 * 2、每次组件调用收费0.03元，每个小程序账户免费体验1000次
	 * 流程：
	 * 1、小程序渲染按钮 <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber(e)"></button>，注意"libVersion"版本"2.21.2"以上
	 * 2、用户同意后，将返回的 e.detail.code 传到服务端
	 * 3、服务端请求微信端，获取微信用户的手机号
	 * </pre>
	 *
	 * @param code	小程序【经用户同意】获取的“手机号获取凭证”
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年06月06日
	 */
	public String getPhoneNumber(String code){
		return doPost(WxMiniProgramConstant.Urls.PHONE, MapSs.getInstance("code", code)).getJSONObject("phone_info").getString("phoneNumber");
	}

	/**
	 * 获取小程序码（永久有效，限100,000个）
	 *
	 * <pre>
	 * 接口：
	 * getQRCode
	 * 流程：
	 * 1、通过接口生成二维码
	 * 2、小程序进入path页，通过query获取参数
	 * </pre>
	 *
	 * @param path		页面路径：非空，最长1024字节，不能带前缀/，可以拼接参数，如 pages/index/index?t=1
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年06月06日
	 */
	public byte[] getQRCode(String path){
		return doPostBytes(WxMiniProgramConstant.Urls.QR, MapSo.getInstance("path",path));
	}

	/**
	 * 获取小程序码（永久有效，不限数量，5000次/分钟）
	 *
	 * <pre>
	 * 接口：
	 * getUnlimitedQRCode
	 * 流程：
	 * 1、通过接口生成二维码
	 * 2、小程序进入page页时获取scene，进行逻辑处理
	 * 	onLoad (query) { const scene = decodeURIComponent(query.scene)  }
	 *	onLaunch (route) { const scene = decodeURIComponent(route.query.scene)  }
	 * </pre>
	 *
	 * @param page		页面路径：默认主页，不能带前缀/，不能带参数，如 pages/index/index
	 * @param scene		附加参数：必传，作为query参数传递给小程序，最大32个字符，只支持数字，大小写英文以及部分特殊字符
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年06月06日
	 */
	public byte[] getUnlimitedQRCode(String page, String scene){
		return doPostBytes(WxMiniProgramConstant.Urls.UNLIMITED_QR, MapSo.getInstance("page",page,"scene",scene));
	}

	/**
	 * 获取小程序码（永久有效，不限数量，5000次/分钟，主页）
	 *
	 * @param scene		附加参数：必传，作为query参数传递给小程序，最大32个字符，只支持数字，大小写英文以及部分特殊字符
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年06月06日
	 */
	public byte[] getUnlimitedQRCode(String scene){
		return getUnlimitedQRCode(null, scene);
	}


	/**
	 * 获取小程序码（有效期30天，发布后才能生成）
	 *
	 * <pre>
	 * 接口：
	 * generateUrlLink
	 * </pre>
	 *
	 * @param path	页面路径：默认主页，带前缀/，不能带参数，如 /pages/index/index
	 * @param query	查询参数，最大1024个字符
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年06月06日
	 */
	public String getUrlLink(String path, String query){
		return doPost(WxMiniProgramConstant.Urls.LINK, MapSo.getInstance("path",path,"query",query)).getString("url_link");
	}



	/**
	 * 发起GET请求
	 *
	 * @param url
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年05月31日
	 */
	private JSONObject doGet(String url){
		// 拼接 appid_secret
		url += "&appid=" + wxMiniProgramProperties.getAppid() + "&secret=" + wxMiniProgramProperties.getSecret();
		// 发起请求
		return response(url, HttpClientUtil.doGet(url));
	}

	/**
	 * 发起POST请求
	 *
	 * @param url		请求URL
	 * @param params	请求参数
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年05月31日
	 */
	private JSONObject doPost(String url, Object params){
		// 拼接 access_token
		url += "?access_token=" + this.getAccessToken();
		// 发起请求
		return response(url, HttpClientUtil.doPostJson(url, JsonUtil.toString(params)));
	}

	/**
	 * 发起POST请求 - 文件
	 *
	 * @param url		请求URL
	 * @param params	请求参数
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年05月31日
	 */
	private byte[] doPostBytes(String url, Object params){
		// 拼接 access_token
		url += "?access_token=" + this.getAccessToken();
		// 发起请求
		return response(url, HttpClientUtil.doPostJsonBytes(url, JsonUtil.toString(params)));
	}

	/**
	 * 读取响应
	 *
	 * @param url
	 * @param result
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年05月31日
	 */
	private static JSONObject response(String url, String result){
		// 读取请求结果
		JSONObject res = JsonUtil.parseObject(result);
		if (isSuccess(res)) {
			// 成功
			return res;
		}else {
			// 失败
			throw error(url, res);
		}
	}

	/**
	 * 读取响应
	 *
	 * @param url
	 * @param result
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2023年05月31日
	 */
	private static  byte[] response(String url, byte[] result){
		// 读取请求结果
		if (result[0] == '{' && result[result.length-1] == '}') {
			// 失败
			throw error(url, JsonUtil.parseObject(StringUtil.getStr(result)));
		}
		return result;
	}

	/**
	 * 判断响应是否成功
	 * 
	 * @param res	响应结果
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年8月22日
	 */
	private static boolean isSuccess(JSONObject res) {
		// 响应码
		String status = res.getString(WxMiniProgramConstant.Response.STATUS);
		// 成功：没有响应码、响应码为0
		return StringUtils.isBlank(status) || WxMiniProgramConstant.Response.SUCCESS_STATUS.equals(status);
	}
	
	/**
	 * 失败处理
	 * 
	 * @param url	请求的URL
	 * @param res	响应结果
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年8月22日
	 */
	private static BizRuntimeException error(String url, JSONObject res) {
		// 剪除密钥
		int index = url.indexOf("&secret=");
		if (index > 0) {
			url = url.substring(0, index);
		}
		// 打印日志，抛异常
		LOG.error(WxMiniProgramLogConstant.RESPONSE_ERROR, url, res);
		return new BizRuntimeException(res.getString(WxMiniProgramConstant.Response.MSG));
	}

}
